﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Reflection;
using System.ComponentModel;
using System.IO;

namespace ConvertToExcel
{
    #region CsvParams
    /// <summary>
    /// Summary description for CsvFileDescription
    /// </summary>
    public class CsvParams
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="HasColumnName"></param>
        /// <param name="splitChar"></param>
        /// <param name="encoding"></param>
        /// <param name="fileCultureName"></param>
        public CsvParams(bool HasColumnName, char splitChar, Encoding encoding, string fileCultureName)
        {
            ContainFieldName = HasColumnName;
            SeparatorChar = splitChar;
            TextEncoding = encoding;
            FileCultureName = fileCultureName;
            EnforceFieldAttribute = false;
            DetectEncodingFromByteOrderMarks = true;
        }

        // Culture used to process the CSV values, specifically numbers and dates.
        private CultureInfo _cultureInfo=CultureInfo.CurrentCulture;

        // FileCultureName and FileCultureInfo both get/set
        // the CultureInfo used for the file.
        // For example, if the file uses Dutch date and number formats
        // while the current culture is US English, set
        // FileCultureName to "nl-NL".
        // To simply use the current culture, leave the culture as is.
        //
        public string FileCultureName
        {
            get { return _cultureInfo.Name; }
            set { _cultureInfo = new CultureInfo(value); }
        }

        public CultureInfo FileCultureInfo
        {
            get { return _cultureInfo; }
            set { _cultureInfo = value; }
        }

        // Character used to separate fields in the file.
        // By default, this is comma (,).
        // For a tab delimited file, you would set this to
        // the tab character ('\t').
        public char SeparatorChar { get; set; }

        // If true, then:
        // When writing a file, the column names are written in the
        // first line of the new file.
        // When reading a file, the column names are read from the first
        // line of the file.
        //
        public bool ContainFieldName { get; set; }

        // If true, only public fields and properties with the
        // [CsvField] attribute are recognized.
        // If false, all public fields and properties are used.
        //
        public bool EnforceFieldAttribute { get; set; }

        // Character encoding. Defaults should work in most cases.
        // However, when reading or writing non-English files, you may want to use
        // Unicode encoding.
        public Encoding TextEncoding { get; set; }
        public bool DetectEncodingFromByteOrderMarks { get; set; }
    }
    #endregion

    #region CsvFieldAttribute
    /// <summary>
    /// Summary description for CsvColumnAttribute
    /// </summary>
    [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = false)]
    public class CsvFieldAttribute : System.Attribute
    {
        internal const int _DefaultFieldIndex = Int32.MaxValue;

        public string Name { get; set; }
        public bool Nullable { get; set; }
        public int Index { get; set; }
        public NumberStyles NumberStyle { get; set; }
        public string OutputFormat { get; set; }

        public CsvFieldAttribute()
        {
            Name = "";
            Index = _DefaultFieldIndex;
            Nullable = true;
            NumberStyle = NumberStyles.Any;
            OutputFormat = "G";
        }

        public CsvFieldAttribute(
                    string name,
                    int fieldIndex,
                    bool canBeNull,
                    string outputFormat,
                    NumberStyles numberStyle)
        {
            Name = name;
            Index = fieldIndex;
            Nullable = canBeNull;
            NumberStyle = numberStyle;
            OutputFormat = outputFormat;
        }
    }
    #endregion

    internal class CsvStream
    {
        private TextReader _instream;
        private TextWriter _outStream;
        private char _SeparatorChar;
        private char[] _SpecialChars;

        // Current line number in the file. Only used when reading a file, not when writing a file.
        private int _lineNbr;

        /// CsvStream
        /// 
        public CsvStream(TextReader inStream, TextWriter outStream, char SeparatorChar)
        {
            _instream = inStream;
            _outStream = outStream;
            _SeparatorChar = SeparatorChar;
            _SpecialChars = ("\"\x0A\x0D" + _SeparatorChar.ToString()).ToCharArray();
            _lineNbr = 1;
        }

        /// <summary>
        /// WriteRow
        /// </summary>
        /// <param name="row"></param>
        /// <param name="quoteAllFields"></param>
        public void WriteRow(List<string> row, bool quoteAllFields)
        {
            bool firstItem = true;
            foreach (string item in row)
            {
                if (!firstItem) { _outStream.Write(_SeparatorChar); }

                // If the item is null, don't write anything.
                if (item != null)
                {
                    // If user always wants quoting, or if the item has special chars
                    // (such as "), or if item is the empty string or consists solely of
                    // white space, surround the item with quotes.

                    if ((quoteAllFields ||
                        (item.IndexOfAny(_SpecialChars) > -1) ||
                        (item.Trim() == "")))
                    {
                        _outStream.Write("\"" + item.Replace("\"", "\"\"") + "\"");
                    }
                    else
                    {
                        _outStream.Write(item);
                    }
                }

                firstItem = false;
            }

            _outStream.WriteLine("");
        }

        /// <summary>
        /// ReadRow
        /// </summary>
        /// <param name="row">
        /// Contains the values in the current row, in the order in which they 
        /// appear in the file.
        /// </param>
        /// <returns>
        /// True if a row was returned in parameter "row".
        /// False if no row returned. In that case, you're at the end of the file.
        /// </returns>
        public bool ReadRow(ref IDataRow row)
        {
            row.Clear();

            while (true)
            {
                // Number of the line where the item starts. Note that an item
                // can span multiple lines.
                int startingLineNbr = _lineNbr;

                string item = null;

                bool moreAvailable = GetNextItem(ref item);
                if (!moreAvailable)
                {
                    return (row.Count > 0);
                }
                row.Add(new DataRowItem(item, startingLineNbr));
            }
        }

        private bool EOS = false;
        private bool EOL = false;
        private bool previousWasCr = false;

        private bool GetNextItem(ref string itemString)
        {
            itemString = null;
            if (EOL)
            {
                // previous item was last in line, start new line
                EOL = false;
                return false;
            }

            bool itemFound = false;
            bool quoted = false;
            bool predata = true;
            bool postdata = false;
            StringBuilder item = new StringBuilder();

            while (true)
            {
                char c = GetNextChar(true);
                if (EOS)
                {
                    if (itemFound) { itemString = item.ToString(); }
                    return itemFound;
                }

                // ---------
                // Keep track of line number. 
                // Note that line breaks can happen within a quoted field, not just at the
                // end of a record.

                // Don't count 0D0A as two line breaks.
                if ((!previousWasCr) && (c == '\x0A'))
                {
                    _lineNbr++;
                }

                if (c == '\x0D')
                {
                    _lineNbr++;
                    previousWasCr = true;
                }
                else
                {
                    previousWasCr = false;
                }

                // ---------

                if ((postdata || !quoted) && c == _SeparatorChar)
                {
                    // end of item, return
                    if (itemFound) { itemString = item.ToString(); }
                    return true;
                }

                if ((predata || postdata || !quoted) && (c == '\x0A' || c == '\x0D'))
                {
                    // we are at the end of the line, eat newline characters and exit
                    EOL = true;
                    if (c == '\x0D' && GetNextChar(false) == '\x0A')
                    {
                        // new line sequence is 0D0A
                        GetNextChar(true);
                    }

                    if (itemFound) { itemString = item.ToString(); }
                    return true;
                }

                if (predata && c == ' ')
                    // whitespace preceeding data, discard
                    continue;

                if (predata && c == '"')
                {
                    // quoted data is starting
                    quoted = true;
                    predata = false;
                    itemFound = true;
                    continue;
                }

                if (predata)
                {
                    // data is starting without quotes
                    predata = false;
                    item.Append(c);
                    itemFound = true;
                    continue;
                }

                if (c == '"' && quoted)
                {
                    if (GetNextChar(false) == '"')
                    {
                        // double quotes within quoted string means add a quote       
                        item.Append(GetNextChar(true));
                    }
                    else
                    {
                        // end-quote reached
                        postdata = true;
                    }

                    continue;
                }

                // all cases covered, character must be data
                item.Append(c);
            }
        }

        private char[] buffer = new char[4096];
        private int pos = 0;
        private int length = 0;

        private char GetNextChar(bool eat)
        {
            if (pos >= length)
            {
                length = _instream.ReadBlock(buffer, 0, buffer.Length);
                if (length == 0)
                {
                    EOS = true;
                    return '\0';
                }
                pos = 0;
            }
            if (eat)
                return buffer[pos++];
            else
                return buffer[pos];
        }
    }

    #region DataRow
    /// <summary>
    /// Describes a single row. This is made up of a collection of data row items.
    /// Each data row item is of type DataRowItem, containing the actual value (a string) and its line number (an int)
    /// within the file.
    /// </summary>
    public interface IDataRow
    {
        // Number of data row items in the row.
        int Count { get; }

        // Clear the collection of data row items.
        void Clear();

        // Add a data row item to the collection.
        void Add(DataRowItem item);

        // Allows you to access each data row item with an array index, such as
        // row[i]
        DataRowItem this[int index] { get; set; }
    }

    public class DataRow : List<DataRowItem>, IDataRow
    {
    }

    public class DataRowItem
    {
        private string _value;
        private int _lineNbr;

        public DataRowItem(string value, int lineNbr)
        {
            _value = value;
            _lineNbr = lineNbr;
        }

        public int LineNbr
        {
            get { return _lineNbr; }
        }

        public string Value
        {
            get { return _value; }
        }
    }
    #endregion

    #region FieldMapper
    /// <summary>
    /// Summary description for FieldMapper
    /// </summary>
    public class FieldMapper<T> where T :class, new()
    {
        /// <summary>
        /// FieldMapper Constructor
        /// </summary>
        /// <param name="fileDescription"></param>
        public FieldMapper(CsvParams fileDescription, string fileName, bool writingFile)
        {
            if ((!fileDescription.ContainFieldName) &&
                (!fileDescription.EnforceFieldAttribute))
            {
                throw new Exception();
            }
            _fileDescription = fileDescription;
            _fileName = fileName;

            _NameToInfo = new Dictionary<string, TypeFieldInfo>();

            AnalyzeType(typeof(T),!fileDescription.ContainFieldName,writingFile && !fileDescription.ContainFieldName);
        }
        protected class TypeFieldInfo : IComparable<TypeFieldInfo>
        {
            public int index = CsvFieldAttribute._DefaultFieldIndex;
            public string name = null;
            public bool Nullable = true;
            public NumberStyles inputNumberStyle = NumberStyles.Any;
            public string outputFormat = null;
            public bool hasColumnAttribute = false;

            public MemberInfo memberInfo = null;
            public Type fieldType = null;

            // parseNumberMethod will remain null if the property is not a numeric type.
            // This would be the case for DateTime, Boolean, String and custom types.
            // In those cases, just use a TypeConverter.
            //
            // DateTime and Boolean also have Parse methods, but they don't provide
            // functionality that TypeConverter doesn't give you.

            public TypeConverter typeConverter = null;
            public MethodInfo parseNumberMethod = null;

            public int CompareTo(TypeFieldInfo other)
            {
                return index.CompareTo(other.index);
            }
        }

        // IndexToInfo is used to quickly translate the index of a field
        // to its TypeFieldInfo.
        protected TypeFieldInfo[] _IndexToInfo = null;

        // Used to build IndexToInfo
        protected Dictionary<string, TypeFieldInfo> _NameToInfo = null;

        protected CsvParams _fileDescription;

        // Only used when throwing an exception
        protected string _fileName;

        /// <summary>
        /// AnalyzeTypeField
        /// </summary>
        /// <param name="mi"></param>
        /// <param name="allRequiredFieldsMustHaveFieldIndex"></param>
        /// <param name="allCsvColumnFieldsMustHaveFieldIndex"></param>
        /// <returns></returns>
        private TypeFieldInfo AnalyzeTypeField(MemberInfo mi, bool allRequiredFieldsMustHaveFieldIndex, bool allCsvColumnFieldsMustHaveFieldIndex)
        {
            TypeFieldInfo tfi = new TypeFieldInfo();

            tfi.memberInfo = mi;

            if (mi is PropertyInfo)
            {
                tfi.fieldType = ((PropertyInfo)mi).PropertyType;
            }
            else
            {
                tfi.fieldType = ((FieldInfo)mi).FieldType;
            }

            // parseNumberMethod will remain null if the property is not a numeric type.
            // This would be the case for DateTime, Boolean, String and custom types.
            // In those cases, just use a TypeConverter.
            //
            // DateTime and Boolean also have Parse methods, but they don't provide
            // functionality that TypeConverter doesn't give you.

            tfi.parseNumberMethod =tfi.fieldType.GetMethod("Parse",new Type[] { typeof(String), typeof(NumberStyles), typeof(IFormatProvider) });

            tfi.typeConverter = null;
            if (tfi.parseNumberMethod == null)
            {
                tfi.typeConverter =
                    TypeDescriptor.GetConverter(tfi.fieldType);
            }

            // Process the attributes
            tfi.index = CsvFieldAttribute._DefaultFieldIndex;
            tfi.name = mi.Name;
            tfi.inputNumberStyle = NumberStyles.Any;
            tfi.outputFormat = "";
            tfi.hasColumnAttribute = false;

            foreach (Object attribute in mi.GetCustomAttributes(typeof(CsvFieldAttribute), true))
            {
                CsvFieldAttribute cca = (CsvFieldAttribute)attribute;

                if (!string.IsNullOrEmpty(cca.Name))
                {
                    tfi.name = cca.Name;
                }

                tfi.index = cca.Index;
                tfi.hasColumnAttribute = true;
                tfi.Nullable = cca.Nullable;
                tfi.outputFormat = cca.OutputFormat;
                tfi.inputNumberStyle = cca.NumberStyle;
            }

            if (allCsvColumnFieldsMustHaveFieldIndex &&
                tfi.hasColumnAttribute &&
                tfi.index == CsvFieldAttribute._DefaultFieldIndex)
            {
                throw new Exception();
            }

            if (allRequiredFieldsMustHaveFieldIndex &&
                (!tfi.Nullable) &&
                (tfi.index == CsvFieldAttribute._DefaultFieldIndex))
            {
                throw new Exception();
            }

            return tfi;
        }

        /// <summary>
        /// AnalyzeType
        /// </summary>
        /// <param name="type"></param>
        /// <param name="allRequiredFieldsMustHaveFieldIndex"></param>
        /// <param name="allCsvColumnFieldsMustHaveFieldIndex"></param>
        protected void AnalyzeType(Type type, bool allRequiredFieldsMustHaveFieldIndex, bool allCsvColumnFieldsMustHaveFieldIndex)
        {
            _NameToInfo.Clear();

            // Initialize NameToInfo

            foreach (MemberInfo mi in type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
            {
                // Only process field and property members.
                if ((mi.MemberType == MemberTypes.Field) ||
                    (mi.MemberType == MemberTypes.Property))
                {
                    // Note that the compiler does not allow fields and/or properties
                    // with the same name as some other field or property.
                    TypeFieldInfo tfi =
                        AnalyzeTypeField(
                                mi,
                                allRequiredFieldsMustHaveFieldIndex,
                                allCsvColumnFieldsMustHaveFieldIndex);

                    _NameToInfo[tfi.name] = tfi;
                }
            }

            // Initialize IndexToInfo

            int nbrTypeFields = _NameToInfo.Keys.Count;
            _IndexToInfo = new TypeFieldInfo[nbrTypeFields];

            int i = 0;
            foreach (KeyValuePair<string, TypeFieldInfo> kvp in _NameToInfo)
            {
                _IndexToInfo[i++] = kvp.Value;
            }

            // Sort by FieldIndex. Fields without FieldIndex will 
            // be sorted towards the back, because their FieldIndex
            // is Int32.MaxValue.
            //
            // The sort order is important when reading a file that 
            // doesn't have the field names in the first line, and when
            // writing a file. 
            //
            // Note that for reading from a file with field names in the 
            // first line, method ReadNames reworks IndexToInfo.

            Array.Sort(_IndexToInfo);

            // Make sure there are no duplicate FieldIndices.
            // However, allow gaps in the FieldIndex range, to make it easier to later insert
            // fields in the range.

            int lastFieldIndex = Int32.MinValue;
            string lastName = "";
            foreach (TypeFieldInfo tfi in _IndexToInfo)
            {
                if ((tfi.index == lastFieldIndex) &&
                    (tfi.index != CsvFieldAttribute._DefaultFieldIndex))
                {
                    throw new Exception();
                }

                lastFieldIndex = tfi.index;
                lastName = tfi.name;
            }
        }

        /// <summary>
        /// Assumes that the fields in parameter row are field names.
        /// Reads the names into the objects internal structure.
        /// </summary>
        /// <param name="row"></param>
        /// <param name="firstRow"></param>
        /// <returns></returns>
        ///
        public void ReadNames(IDataRow row)
        {
            // It is now the order of the field names that determines
            // the order of the elements in m_IndexToInfo, instead of
            // the FieldIndex fields.

            // If there are more names in the file then fields in the type,
            // one of the names will not be found, causing an exception.

            for (int i = 0; i < row.Count; i++)
            {
                if (!_NameToInfo.ContainsKey(row[i].Value))
                {
                    // name not found
                    throw new Exception();
                }

                _IndexToInfo[i] = _NameToInfo[row[i].Value];

                if (_fileDescription.EnforceFieldAttribute &&
                    (!_IndexToInfo[i].hasColumnAttribute))
                {
                    // enforcing column attr, but this field/prop has no column attr.
                    throw new Exception();
                }
            }
        }

        /// <summary>
        /// ReadObject
        /// Creates an object of type T from the data in row and returns that object.
        /// </summary>
        /// <param name="row"></param>
        /// <param name="firstRow"></param>
        /// <returns></returns>
        public T ReadObject(IDataRow row)
        {
            if (row.Count > _IndexToInfo.Length)
            {
                // Too many fields
                throw new Exception();
            }
            T obj = new T();
            for (int i = 0; i < row.Count; i++)
            {
                TypeFieldInfo tfi = _IndexToInfo[i];

                if (_fileDescription.EnforceFieldAttribute &&
                        (!tfi.hasColumnAttribute))
                {
                    // enforcing column attr, but this field/prop has no column attr.
                    // So there are too many fields in this record.
                    throw new Exception();
                }

                if ((!_fileDescription.ContainFieldName) &&
                        (tfi.index == CsvFieldAttribute._DefaultFieldIndex))
                {
                    // First line in the file does not have field names, so we're 
                    // depending on the FieldIndex of each field in the type
                    // to ensure each value is placed in the correct field.
                    // However, now hit a field where there is no FieldIndex.
                    throw new Exception();
                }
                // value to put in the object
                string value = row[i].Value;

                if (value == null)
                {
                    if (!tfi.Nullable)
                    {
                        throw new Exception();
                    }
                }
                else
                {
                    try
                    {
                        Object objValue = null;

                        // Normally, either tfi.typeConverter is not null,
                        // or tfi.parseNumberMethod is not null. 
                        // 
                        if (tfi.typeConverter != null)
                        {
                            objValue = tfi.typeConverter.ConvertFromString(
                                            null,
                                            _fileDescription.FileCultureInfo,
                                            value);
                        }
                        else if (tfi.parseNumberMethod != null)
                        {
                            objValue =
                                tfi.parseNumberMethod.Invoke(
                                    tfi.fieldType,
                                    new Object[] { 
                                    value, 
                                    tfi.inputNumberStyle, 
                                    _fileDescription.FileCultureInfo });
                        }
                        else
                        {
                            // No TypeConverter and no Parse method available.
                            // Try direct approach.
                            objValue = value;
                        }

                        if (tfi.memberInfo is PropertyInfo)
                        {
                            ((PropertyInfo)tfi.memberInfo).SetValue(obj, objValue, null);
                        }
                        else
                        {
                            ((FieldInfo)tfi.memberInfo).SetValue(obj, objValue);
                        }
                    }
                    catch (Exception e)
                    {
                        if (e is TargetInvocationException)
                        {
                            e = e.InnerException;
                        }

                        if (e is FormatException)
                        {
                            e = new Exception();
                        }
                    }
                }
            }

            // Visit any remaining fields in the type for which no value was given
            // in the data row, to see whether any of those was required.
            // If only looking at fields with CsvColumn attribute, do ignore
            // fields that don't have that attribute.

            for (int i = row.Count; i < _IndexToInfo.Length; i++)
            {
                TypeFieldInfo tfi = _IndexToInfo[i];

                if (((!_fileDescription.EnforceFieldAttribute) ||
                     tfi.hasColumnAttribute) &&
                    (!tfi.Nullable))
                {
                    throw new Exception();
                }
            }

            return obj;
        }
    }
    #endregion
}
